Next.js 是一個全端框架,除了提供 SSR 與 SSG 的功能之外,還能夠建立 API 提供前端頁面使用。
你可以使用 API routes 建立 REST API,如果有 graphql 的需求,也可以用來建立 graphql API。官方文件提供了許多的範例,讓我們可以快用用一些模板建立服務:
在這篇文章中將以 REST API 作為範例,體驗 Next.js 的 API routes。
API routes 的概念與前端頁面一樣,都是使用 file-based routing,所有的 API 都會放在 pages/api
這個資料夾底下,例如 pages/api/products
即是對應 api/products
這個 endpoint。
在這這資料夾中的所有檔案將不會被當作頁面的 url,因此在 pages/api
中的檔案都不會被打包近客戶端的 bundle 中,如果使用者在瀏覽器的網址列輸入 api/products
,即是跟伺服器端請求 API,而並非是一個頁面。
舉一個例子,在 pages/api/products.ts
中建立一個 API,回傳 json 格式的資料,並且包含 200 的 HTTP 狀態碼:
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ products: [{ name: "item" }] });
}
從以上範例中可以看到,API routes 是一個 export default
的 function,可以使用 res.status
指定 HTTP 的狀態碼,並使用 res.json
回傳資料至客戶端。
接著,在瀏覽器中輸入 api/products
就會看到 API 回應的資料,打開 Chrome 的 Network 也可以看到 HTTP 狀態碼為 200。
我們從 req
與 res
的型別定義中可以看到:
req
的型別 NextApiRequest
繼承了 http.IncomingMessage
,是一個 IncomingMessage
的 instance。res
的型別 NextApiResponse
繼承了 http.ServerResponse
,是一個 ServerResponse
的 instance。但是兩者與原生 node.js 的寫法不太一樣,像是 Next.js 封裝了 req
,讓我們可以用 req.query
取得 url 上的參數,還可以使用 req.body
取得 body 中的內容。此外,Next.js 也封裝了 res
這個物件,讓我們能夠用 chain function 的方式使用 res
,如上方的範例。
如果想要處理不同的 HTTP method,可以透過 req.method
這個屬性判斷:
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") {
res.status(200).json({ products: [{ name: "item" }] });
} else if (req.method === "POST") {
// 建立產品資料
} else if (req.method === "DELETE") {
// 刪除產品資料
}
}
既然 API routes 是基於 file-based routing,所以也能夠處理動態的資源,例如在前面章節實作的「產品詳細頁面」,其對應的頁面是 pages/products/[id].tsx
,在這個頁面中的 id
是動態的,會根據使用者瀏覽的產品對應至不同的值,因此在詳細頁面中也會需要呼叫不同的 API endpoint,像是 api/products/[id]
。
要建立 dynamic API routes 其概念與「頁面」一樣, api/products/[id]
即是對應 api/products/[id].ts
:
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query;
res.status(200).json({ productId: id });
}
接著,在瀏覽器中輸入 api/products/123
就會看到 API 回應的資料,打開 Chrome 的 Network 也可以看到 HTTP 狀態碼為 200。
以部落格的例子來說,一篇貼文的 url 以「年月日」來設計,所以 url 可能這個樣子 /posts/<year>/<month>/<day>
, 如果 API 要像下方這樣子建立很多個資料夾,工程師們大概會覺得很麻煩:
pages/
└── api/
└──posts/
└── [year]/
└── [month]/
└── [day].ts
為了解決這個情況,所以 API routes 也有 catch all routes 的實作,以上方的例子來說,只要定義 /pages/api/[...date].ts
就可以匹配「年月日」的參數,而且甚至可以無限地加上新的參數,例如顆粒度想要細到小時、分鐘、秒,都是可以的:
pages/
└── api/
└──posts/
└── [...date].ts
[...date]
的資料最後會以陣列被儲存在 router.query
中:
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { date } = req.query;
res.status(200).json({ date });
}
以 /api/posts/2021/12/31
這個例子來說, date
會是以下這個模樣:
{
date: [2021, 12, 31];
}
如果一個 API 的資料夾同時包括 [id].ts
與 [...date].ts
兩種 pattern 的話,當呼叫 /api/posts/abc
會先匹配 /api/posts/[id].ts
這個 API routes,而 2 個以上的參數才會匹配 [...date].ts
。
這是 dynamic routes 的最後一個 pattern,前面提到的 [id].ts
與 [...date].ts
都不能用來匹配 /api/products
這種 API endpoint,但是 optional catch all API routes 可以用來匹配所有的 API endpoint,它是以兩個鐘括號作為定義,例如 [[...slug]].ts
。
所以,如果想要用一個 API routes 定義部落格中所有的貼文 API,則可以定義 /api/posts/[[...slug]].ts
,這一個 API 則可以同時匹配以下幾種 API endpoints:
/api/posts
/api/posts/123
/api/posts/2021/12/31
而這幾個 API endpoints 的 req.query
會是以下這個樣子:
{}
{ slug: [123] }
{ slug: [2021, 12, 31] }
Next.js 為了讓 API 定義更彈性一點,提供了各種不同的 API routes 的定義模式,包括以下幾種:
/api/posts.ts
/api/posts/[id].ts
/api/posts/[...date].ts
/api/posts/[[...slug]].ts
它們彼此之前的關係是由上到下,後面的 API routes 不會蓋掉前面,舉例來說如果同時在一個 API routes 的資料夾有四種不同的模式:
/api/posts/about.ts
匹配 /api/posts/about
/api/posts/[id].ts
匹配 /api/posts/123
/api/posts/[...date].ts
匹配 /api/posts/2021/12/31
/api/posts/[[...slug]].ts
不會匹配任何的 endpoints所以用這種方式思考一個特殊的情況,當一個 API routes 的 API routes 只有 [id].ts
跟 [[...slug]].ts
,但是沒有 index.ts
,直覺的思考「是不是 [[...slug]].ts
會匹配 /api/posts
這種 endpoint」,但因為有 [id].ts
存在於 API routes 的資料夾中, /api/posts
將會回傳 HTTP 404。
想要讓 [[...slug]].ts
可以匹配 /api/posts
,則要刪除 [id].ts
這個 API routes,讓 [[...slug]].ts
做所有的事情。
現在我們了解了幾種不同定義 API routes 的模式後,來嘗試設計「產品列表頁面」與「產品詳細頁面」中需要的 API,已知有兩個 API endpoints :
/api/products
:回傳產品列表/api/products/[id]
:回傳一個產品的詳細資訊所以,我們可以統整出幾種不同的定義方式:
/api/products.ts
/api/products/[id].ts
/api/products/index.ts
/api/products/[id].ts
/api/products/[[...slug]].ts
方法一與方法二的 [id].ts
可以用 [...slug].ts
或 [[...slug].ts
取代,但是如果使用[[...slug]].ts
則沒有意義,因為 /api/products
已經由 index.ts
定義了,所以不如使用 [...slug].ts
避免造成 API routes 混亂。
今天我們了解了如何定義 API routes,可以藉由 req.method
判斷 API 的 HTTP method,用以切分不同的實作,想要獲得 endpoint 上的資訊,則可以透過 req.query
取得。當我們要回應 API 請求時,可以使用 res.status
定義 HTTP 狀態碼,並透過 res.json
回傳 JSON 格式的資料。
而 API routes 有四種模式匹配各種路由,基本上是 file-based routing 的概念,只是套用在 API 身上。而每一種模式都有其先後順序,在實作時要注意,否則可能會造成 API routes 看起來很混亂,讓後續維護 API 時感到困擾。